分治法是将问题分解为两个互不相交的子问题。如二叉树的左右子树遍历等;
动态规划应用于子问题存在重叠的情况,如最大子数组和,最长公共子序列等;动态规划就是在递归中记录了子问题的解,逐步重构最优解。
动态规划一般由两种方法来实现,一种为自顶向下的备忘录方式,用递归实现,一种为自底向上的方式,用迭代实现。
暴力递归
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
动态规划
- 从暴力递归中来
- 将每一个子问题的解记录下来,不断重构最优解
- 最终得到问题的解
- 把暴力递归的过程,抽象成了状态表达
本质上状态空间的状态转移。
所谓状态转移是指每个阶段的最优状态(对应于子问题的解)可以从之前的某一个或几个阶段的状态中得到,即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解,这个性质叫做最优子结构。而不管之前这个状态是如何得到的,这被称之为无后效性。
关键在于:如何求解递推式!
- 如何设置状态
- 如何递推,从上一个状态如何转化到下一个状态
- 最终达到最优解
背包问题
01背包
题目:
有N件物品和一个空间为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
例如:有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,每件物品数量只有一个,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和。
特点:每种物品仅有一件,可以选择放或不放
分析:
只放一件物品价值最大,选取在承重范围内的最大值放进背包
只放两件物品价值最大,两种情况:1,在放一件物品最大后,在剩余承重下再放一件;2,重新选择两件物品。这两种情况谁价值大选谁。
自顶向下分析:
两个变量:物品数i和空间j,
一个所求:背包最大价值。
数组$W[i][j]$表示有i个物品可以选择放入空间为j的背包中的最大价值,注意是选择放入不是全部放入背包
当i=N且j=V时就是所求。
所以,$W[i][j]$可以由子问题$W[i-1][j]$,即有i-1个物品可选择放入空间为j的背包中的最大价值,加上第i个物品构成。有两种情况:
第一:第i个物品放入背包,$W[i][j]=W[i-1][j-c_i]+w_i$
第二:第i个物品不放入背包,$W[i][j]=W[i-1][j]$
取两者最大值就是i个物品可以选择放入空间为j的背包中的最大价值$W[i][j]$
自底向上求解:
由i=0,j=0开始逐步求解$W[i][j]$
1 | int knapsack(int *value,int *cost,int n,int v){ |
时间复杂度:$O(NV)$
空间复杂度:$O(NV)$
空间优化:只需要一个一维数组就可以保存需要的数据。空间复杂度可以优化到$O(V)$
上面的方法是用一个二维数组保存子问题的解(i-1个物品放入空间为j的背包的最大价值)
在做递推的时候只用了i-1行的数据,所以可以使用一个一位数组只保存i-1行的数组。
1 | int knapack(const vector<int> value,const vector<int> cost,int n,int v){ |
空间v的遍历一定是逆序。这样才能保证$W[j-cost[i]]$不会在j的一次循环内被更新,保证了物品只能使用一次。比如,
完全背包
题目:
有N种物品和一个空间为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包空间,且价值总和最大。
分析:
N种物品无限使用,也就相当于少了一个限制,求空间为V的背包最大价值是多少。
$W[V]$表示空间为V的书包的最大价值。
$W(V)=argmax(W(V-c_i)+w_i)$
1 | int knapack_comlete(const vector<int> value,const vector<int> cost,int n,int v){ |
多重背包
题目:
有N种物品和一个空间为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包空间,且价值总和最大。
分析:
对于第i个物品,
$$
W[i][j]=max(W[i-1][j],W[i-1][j-kc_i]+kc_i)
$$
其中,$k$的取值范围是$(1,n[i])$.
时间复杂度是$O(V*\sum(n[i]))$
最长公共子序列
题目:
给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。
分析:
公共子序列 : 如果序列C既是序列A的子序列,同时也是序列B的子序列,则称它为序列A和序列B的公共子序列。空序列是任何两个序列的公共子序列。
- 确定动态规划问题,
- 寻找DP数组,一维or二维。题目包含两个变量,字符串1和字符串2。
- 确定递推式。关于字符串的变量一般就是长度。两个字符串的长度变化对最长子序列的结果都有影响。确定$DP[i][j]$表示字符串1中长度为i时与字符串2中长度为j的最长公共子序列的长度。
- $DP[i][j]$可以由三部分($DP[i-1][j],DP[i][j-1],DP[i-1][j-1]$)递推得到,所以,有
$$
DP[i][j]=max(DP[i-1][j],DP[i][j-1]),if(str1[i]!=str2[j])
$$
$$
DP[i][j]=DP[i-1][j-1]+1,if(str1[i]=str2[j])
$$
1 | int MaxCommonSeq(string str1,string str2){ |